/*
 *   Copyright 2021 zzh
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */
package com.aduib.boot.common.SPI;

import com.aduib.boot.common.exception.CommonException;
import com.aduib.boot.common.log.LOG;
import com.aduib.boot.common.util.ClassUtils;
import com.aduib.boot.common.util.StrUtils;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/** Code generator for Adaptive class */
public class AdaptiveClassCodeGenerator {

  private static final String CODE_PACKAGE = "\npackage %s;\n";

  private static final String CODE_IMPORTS = "import %s;\n";

  private static final String CODE_CLASS_DECLARATION = "public class %s$Adaptive implements %s {\n";

  private static final String CODE_FIELD_DECLARATION =
      "  private %s %s=com.aduib.boot.common.SPI.SPILoader.INSTANCE;\n";

  private static final String CODE_METHOD_DECLARATION = "  public %s %s(%s) %s {\n%s}\n";

  private static final String CODE_METHOD_ARGUMENT = "%s arg%d";

  private static final String CODE_METHOD_THROWS = "throws %s";

  private static final String CODE_UNSUPPORTED =
      "throw new UnsupportedOperationException(\"The method %s of interface %s is not adaptive method!\");\n";

  private static final String CODE_SPI_NAME_NULL_CHECK =
      "if(%s == null) "
          + "throw new CommonException(\"Failed to get SPI (%s) name from type (%s) use keys(%s)\");\n";

  private static final String CODE_SPI_VAR = "%s spi = null;\n";

  private static final String CODE_SPI_ASSIGNMENT =
      "if(spi==null) {try {\n spi = (%s)%s.getSPI(%s.class,\"%s\"); \n} catch (Throwable t) { t.printStackTrace(); }\n}\n";

  private static final String CODE_SPI_NULL_CHECK = "if(spi==null) return %s;\n";

  private static final String CODE_EXTENSION_METHOD_INVOKE_ARGUMENT = "arg%d";

  private final Class<?> type;

  private String defaultExtName;

  public AdaptiveClassCodeGenerator(Class<?> type, String defaultExtName) {
    this.type = type;
    this.defaultExtName = defaultExtName;
    try {

    } catch (Exception e) {

    }
  }

  /** test if given type has at least one method annotated with <code>Adaptive</code> */
  private boolean hasAdaptiveMethod() {
    return Arrays.stream(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Adaptive.class));
  }

  /** generate and return class code */
  public String generate() {
    // no need to generate adaptive class since there's no adaptive method found.
    if (!hasAdaptiveMethod()) {
      throw new CommonException(
          "No adaptive method exist on extension "
              + type.getName()
              + ", refuse to create the adaptive class!");
    }

    StringBuilder code = new StringBuilder();
    code.append(generatePackageInfo());
    code.append(generateImports());
    code.append(generateClassDeclaration());
    Class<? extends SPIFactory> spiFactoryClass = SPIFactory.class;
    code.append(generateField(spiFactoryClass.getName(), spiFactoryClass.getSimpleName()));
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      code.append(generateMethod(method));
    }
    code.append("}");
    LOG.d(code.toString());
    return code.toString();
  }

  /** generate package info */
  private String generatePackageInfo() {
    return String.format(CODE_PACKAGE, type.getPackage().getName());
  }

  /** generate imports */
  private String generateImports() {
    return String.format(CODE_IMPORTS, SPIFactory.class.getName());
  }

  /** generate class declaration */
  private String generateClassDeclaration() {
    return String.format(CODE_CLASS_DECLARATION, type.getSimpleName(), type.getCanonicalName());
  }

  /** generate method not annotated with Adaptive with throwing unsupported exception */
  private String generateUnsupported(Method method) {
    return String.format(CODE_UNSUPPORTED, method, type.getName());
  }

  private String generateField(String fieldType, String fieldName) {
    return String.format(CODE_FIELD_DECLARATION, fieldType, fieldName);
  }
  /** generate method declaration */
  private String generateMethod(Method method) {
    String methodReturnType = method.getReturnType().getCanonicalName();
    String methodName = method.getName();
    String methodContent = generateMethodContent(method);
    String methodArgs = generateMethodArguments(method);
    String methodThrows = generateMethodThrows(method);
    return String.format(
        CODE_METHOD_DECLARATION,
        methodReturnType,
        methodName,
        methodArgs,
        methodThrows,
        methodContent);
  }

  /** generate method arguments */
  private String generateMethodArguments(Method method) {
    Class<?>[] pts = method.getParameterTypes();
    return IntStream.range(0, pts.length)
        .mapToObj(i -> String.format(CODE_METHOD_ARGUMENT, pts[i].getCanonicalName(), i))
        .collect(Collectors.joining(", "));
  }

  /** generate method throws */
  private String generateMethodThrows(Method method) {
    Class<?>[] ets = method.getExceptionTypes();
    if (ets.length > 0) {
      String list =
          Arrays.stream(ets).map(Class::getCanonicalName).collect(Collectors.joining(", "));
      return String.format(CODE_METHOD_THROWS, list);
    } else {
      return "";
    }
  }

  /** generate method content */
  private String generateMethodContent(Method method) {
    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
      return generateUnsupported(method);
    } else {
      String[] values = FactoryUtils.getMethodAdaptiveValue(adaptiveAnnotation,type);

      // check extName == null?
      //      for (String value : values) {
      //        code.append(generateSpiNameNullCheck(value, values));
      //      }

      code.append(generateSpiAssignment(values));

      // return statement
      code.append(generateReturn(method));
    }

    return code.toString();
  }

  /** generate code for variable spiName null check */
  private String generateSpiNameNullCheck(String value, String[] values) {
    return String.format(
        CODE_SPI_NAME_NULL_CHECK, value, value, type.getName(), Arrays.toString(values));
  }

  /**
   * @return
   * @param values
   */
  private String generateSpiAssignment(String[] values) {
    StringBuilder sb = new StringBuilder();
    sb.append(String.format(CODE_SPI_VAR, type.getName()));
    for (String value : values) {
      sb.append(
          String.format(
              CODE_SPI_ASSIGNMENT,
              type.getName(),
              SPIFactory.class.getSimpleName(),
              type.getName(),
              value));
    }
    if (StrUtils.isNotBlank(defaultExtName)) {
      sb.append(
          String.format(
              CODE_SPI_ASSIGNMENT,
              type.getName(),
              SPIFactory.class.getSimpleName(),
              type.getName(),
              defaultExtName));
    }
    return sb.toString();
  }

  /** generate method invocation statement and return it if necessary */
  private String generateReturn(Method method) {
    String returnStatement = method.getReturnType().equals(void.class) ? "" : "return ";
    StringBuilder sb = new StringBuilder();
    if (StrUtils.isNotBlank(returnStatement)) {
      sb.append(String.format(CODE_SPI_NULL_CHECK, generateReturnValue(method)));
    }
    String args =
        IntStream.range(0, method.getParameters().length)
            .mapToObj(i -> String.format(CODE_EXTENSION_METHOD_INVOKE_ARGUMENT, i))
            .collect(Collectors.joining(", "));

    return sb.append(returnStatement)
        .append(String.format("spi.%s(%s);\n", method.getName(), args))
        .toString();
  }

  private String generateReturnValue(Method method) {
    Class<?> returnType = method.getReturnType();
    if (ClassUtils.isPrimitive(returnType)) {
      return ClassUtils.getPrimitiveTypeValue(returnType).toString();
    }
    return "null";
  }
}
